package FIT::Document::Parser::Twig;

use strict;
use warnings;
use aliased 'XML::Twig';

sub make_twig() { Twig->new(pretty_print => 'none') }

package XML::Twig::Elt;

use aliased 'FIT::Document::Parser::Text';

# generic element methods ------------------------------------------------------

sub fit_is_empty { shift->fit_text !~ /\S/ }

sub fit_text {
    my $element = shift;
    my $xml = $element->inner_xml;
    $xml =~ s/<\s*table\s+.*//gis; # up to 1st nested table, for fixture args
    return Text->html_to_text($xml);
}

# TODO: should be- negative not positive. remove only special UTF-8 symbols
# or else we miss out on Perl compatible non-ASCII identifiers
sub fit_identifier {
    local $_ = shift->fit_text;
    s/ /_/g;
    s/\./::/g;
    s/[^a-zA-Z0-9_:]//g;
    return $_;
}

sub has_errors {
    my $element = shift;
    return $element->descendants( q{td[@class="fit_error"]} )? 1: 0;
}

sub document_body {
    my $element = shift;
    my $body = $element->parent('body');
    return $body if $body;

    my $html = $element->parent('html');
    die "Can't handle document with no HTML tag" unless $html;
    $body = XML::Twig::Elt->new('body');
    $body->merge($html); # assumes no head as well if no body
    $body->paste(last_child => $html);
    return $body;
    
}

sub fit_input_columns {
    my $e = shift;
    $e = $e->parent('table') unless $e->tag eq 'table';
    local $_;
    return 
        map { $_->fit_identifier }
        grep { !$_->fit_is_output_column_heading }
        $e->second_row->cells;
}

# table methods ----------------------------------------------------------------

sub first_table  { shift->first_descendant('table') }
sub next_table   { shift->next_sibling('table')     }
sub first_row    { shift->first_descendant('tr')    }
sub first_cell   { shift->first_descendant('td')    }
sub last_cell    { shift->last_descendant('td')     }
sub last_row     { shift->last_descendant('tr')     }
sub rows         { shift->descendants('td')         }
sub second_row   { shift->first_row->next_row       }
sub fixture_name { shift->first_cell->fit_text      }

sub fixture_args {
    my $table = shift;
    # fixture args can be cells in 1st row, or nested table in 1st cell
    my $is_nested = $table->first_cell->first_table;
    local $_;
    my @cells = $is_nested?
        $table->first_table->cells:
        $table->first_row->cells;
    shift @cells unless $is_nested; # don't include fixture name
    return map { $_->fit_text } @cells;
}

# expects table to have 1st row with fixture name
sub column_headings {
    my $table = shift;
    local $_;
    map { $_->fit_identifier } $table->second_row->cells;
}

sub get_body {
    my $table = shift;
    my $first_child = $table->first_child;
    return $first_child if $first_child && $first_child->tag eq 'tbody';
    my $body = XML::Twig::Elt->new('tbody');
    $body->merge($table);
    $body->paste(first_child => $table);
    return $body;
}

# row methods ------------------------------------------------------------------

sub cells        { shift->descendants('td')  }
sub next_row     { shift->next_sibling('tr') }
sub previous_row { shift->prev_sibling('tr') }
sub row_index    { shift->parent->pos - 1    } # XPath pos starts at 1

# cell methods -----------------------------------------------------------------

sub column_index  { shift->pos - 1            } # XPath pos starts at 1
sub previous_cell { shift->prev_sibling('td') }
sub next_cell     { shift->next_sibling('td') }
sub row           { shift->parent('tr')       }
sub table         { shift->parent('table')    }

sub fit_is_output_column_heading {
    my $cell = shift;
    return $cell->trimmed_text =~ /\?/;
}

sub fit_is_output_cell {
    my $cell = shift;
    return $cell->column_heading_cell->fit_is_output_column_heading;
}

sub detect_mark {
    my ($cell, $mark) = @_;
    $cell = $cell->first_cell unless $cell->tag eq 'td';

    # child tables will have their own markers and handlers
    # TODO this bug prevents markers on cells with nested tables
    # SOLVE by forcing markers to be 1st chars in cell- easy to detect
    return 0 if $cell->descendants('table');

    return $cell->trimmed_text =~ /$mark/; # TODO: 1st char only?
}

sub clear_mark {
    my ($cell, $mark) = @_;
    $cell = $cell->first_cell unless $cell->tag eq 'td';

    my $xml = $cell->inner_xml;
    $xml =~ s/$mark//g;
    $cell->set_inner_xml($xml);
}

sub insert_mark {
    my ($cell, $mark) = @_;
    $cell = $cell->first_cell unless $cell->tag eq 'td';

    $cell->set_inner_xml("$mark ". $cell->inner_xml);
}

sub cell_above {
    my $cell = shift;
    my $previous_row = $cell->row->previous_row;
    die "No row above [". $cell->sprint. "]" unless $previous_row;
    my $cell_above = $previous_row->child( $cell->pos - 1);
    die "No cell above [". $cell->sprint. "]" unless $cell_above;
    return $cell_above;
}

# expects table to have 1st row with fixture name
sub column_heading_cell {
    my $cell = shift;
    my $index = $cell->column_index;
    return ( $cell->table->second_row->cells )[$index];
}

sub column_heading {
    my $cell = shift;
    return $cell->column_heading_cell->fit_identifier;
}

sub replace_text {
    my ($cell, $new_text) = @_;
    $cell->cut_children;
    $cell->set_text($new_text);
}

# ------------------------------------------------------------------------------

1;

=head1 NAME

FIT::Document::Parser::Twig - add FIT specific methods to XML::Twig::Elt


=head1 SYNOPSIS

  use aliased 'FIT::Document::Parser::Twig';
  use aliased 'FIT::Assert';

  my $twig = Twig->make_twig(q{
      <html>
          <table>
              <tr>
                  <td>a</td>
                  <td>b</td>
              </tr>
          </table>
          <table>
              <tr><td>c</td></tr>
              <tr><td>d</td></tr>
              <tr><td>e</td></tr>
          </table>
          <table>
            <tr>
                <td>not empty</td>
                <td></td>
            </tr>
      </html>
  });

  my $table_1 = $twig->root->table_1;
  my $table_2 = $table_1->next_table;
  my $table_3 = $table_2->next_table;

  print $table_1->first_cell->fit_text;                               # a
  print $table_1->fixture_name;                                       # a
  print $table_2->fixture_name;                                       # c
  print( ($table_1->fixture_args)[0] );                               # b
  print( ($table_2->column_headings)[0] );                            # d
                                                                     
  print $table_1->first_row->last_cell->fit_text;                     # b
  print $table_2->first_row->row_index;                               # 0
  print $table_2->first_row->next_row->row_index;                     # 1
  print $table_2->second_row->row_index;                              # 1
  print $table_2->second_row->previous_row->row_index;                # 0
  print $table_1->first_row->table->fixture_name;                     # a
                                                                     
  print $table_1->first_cell->next_cell->column_index;                # 1
  print $table_1->first_cell->next_cell->previous_cell->column_index; # 0
  print $table_2->second_row->first_cell->cell_above->fit_text;       # c
  print $table_2->second_row->next_row->first_cell->column_heading;   # d

  print $table_3->first_cell->fit_is_empty;                           # 0
  print $table_3->first_cell->next_cell->fit_is_empty;                # 1
  print $table_3->first_cell->fit_identifier;                         # not_empty

  # content of 1st cell will be the string "moo"
  $table1->first_cell->replace_text('moo');

  # make or get the table tbody element
  my $table_body = $table_1->get_body;

  # get the HTML document body element from any element
  my $html_body = $table_1->document_body;


=head1 DESCRIPTION

Use C<make_twig()> to create a Twig. Pass the HTML as the only parameter.

Then you can use the table-specific methods listed in the SYNOPSIS on any
element you get from the Twig.


=cut
